<doc>[sdnController]: integraion zns into cloud#3711
<doc>[sdnController]: integraion zns into cloud#3711MatheMatrix wants to merge 18 commits into5.5.12from
Conversation
Resolves: ZCF-1365 Change-Id: I73687962636e7871626e687761626d6661716668
Resolves: ZCF-1365 Change-Id: I7262787a6474667a766d77766165796f73717775
Resolves: ZCF-1365 Change-Id: I647469616679716d7366686c77617073746f776c
Resolves: ZCF-1365 Change-Id: I7a61747778757574656967626c6a736366716b6a
<doc>[sdnController]: integraion zns into cloud See merge request zstackio/zstack!9447
Resolves: ZCF-1365 Change-Id: I65767164636167726d6369726f63666b68666477
<fix>[zns]: add ZnsUuidHelper utility for UUID format conversion See merge request zstackio/zstack!9467
Resolves: ZCF-1365 Change-Id: I696f6a6a6e6d7867746e70736d6d646861686166
<fix>[rest]: fix bug<description> See merge request zstackio/zstack!9500
Resolves: ZCF-1365 Change-Id: I73637569786c6d6e6d6479646961726365737074
<fix>[rest]: improve markdown validation error reporting See merge request zstackio/zstack!9529
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough引入 ZNS/Geneve 支持与多处 NIC 生命周期改造:新增异步 Webhook 回调客户端与多项扩展点,重构 VM NIC 分配/释放流程以支持 Before/After/Release SDN 扩展、更新 KVM/NIC 完成逻辑、添加数据库表与错误码,并补充文档与测试工具。 Changes
Sequence Diagram(s)sequenceDiagram
participant VM as VM 创建流程
participant NICF as VmAllocateNicFlow
participant DB as 数据库 (vnicFactory/VO)
participant BEFORE as BeforeAllocateVmNic<br/>扩展
participant SDN as VmAllocateSdnNicFlow
participant AFTER as AfterAllocateSdnNic<br/>扩展
VM->>NICF: 运行分配 NIC 流
NICF->>NICF: allocateMac() / buildNicInventory()
NICF->>DB: vnicFactory.createVmNic()
DB-->>NICF: 返回 VmNicVO
NICF->>BEFORE: callBeforeAllocateVmNicExtensions(nic, spec)
BEFORE-->>NICF: 扩展完成 (Completion)
NICF->>DB: persistStaticIpIfNeeded() / update VO
NICF-->>VM: NIC 分配完成
VM->>SDN: 触发 VmAllocateSdnNicFlow
SDN->>SDN: 查询 AfterAllocateSdnNic 扩展列表
loop 每个扩展
SDN->>AFTER: afterAllocateSdnNic(spec, nics)
AFTER-->>SDN: success / fail
end
SDN-->>VM: 根据错误聚合决定 next 或 fail
sequenceDiagram
participant DET as VmDetachNicFlow / VmReturnReleaseNicFlow
participant BUS as IP 回收总线
participant SDN as callReleaseSdnNics
participant EXT as AfterAllocateSdnNic<br/>扩展
participant DB as 数据库 (删除 VO)
DET->>BUS: return used IPs
DET->>SDN: callReleaseSdnNics(nics, completion)
SDN->>SDN: 查询扩展列表并循环调用
loop 每个扩展
SDN->>EXT: releaseSdnNics(nics)
EXT-->>SDN: 完成(错误记录为 warn)
end
SDN->>DB: 删除 queued nicsToDelete
DB-->>DET: 删除完成
DET->>流程: trigger.next() / chain.next()
sequenceDiagram
participant Client as 外部系统
participant WC as WebhookCallbackClient
participant MAP as 任务注册表 (ConcurrentHashMap)
participant TIME as 超时调度器
participant H as HTTP 回调接收点
Client->>WC: submit(completion, timeout)
WC->>MAP: 存储 taskId -> completion
WC->>TIME: 安排超时任务
WC-->>Client: 返回 taskId
Client->>外部: 执行异步操作
H->>WC: deliverCallback(payload 包含 taskId)
WC->>MAP: 移除并取出 completion
WC->>TIME: 取消超时
WC-->>completion: success / fail
alt 超时触发
TIME->>WC: 调用 fail(taskId, error)
WC->>MAP: 移除并完成失败
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.42.1)utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.javaComment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (4)
header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java (1)
6-11: 建议统一“Release”与“Deleted”语义,避免生命周期阶段歧义。接口/方法名强调
Release,但注释写的是“deleted from database”;建议统一术语或按真实触发时机调整命名/注释,避免扩展点挂错阶段。Based on learnings, 在 ZStack 中扩展点通常按不同执行阶段区分,命名和触发语义需要严格对齐。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java` around lines 6 - 11, The interface AfterReleaseVmNicExtensionPoint and its method afterReleaseVmNic currently mix "Release" in the name with "deleted from database" in the Javadoc; update either the name or the comment so lifecycle semantics match: either rename the interface/method to DeletedVmNicExtensionPoint/afterDeletedVmNic if it is invoked after DB deletion, or change the Javadoc to state the exact "release" stage (e.g., resource release/cleanup before/after DB deletion) if the name is correct; make the change where AfterReleaseVmNicExtensionPoint and afterReleaseVmNic are declared and ensure VmNicInventory references and any callers/registrations are updated consistently to avoid mismatched lifecycle semantics.testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy (1)
62-64: 建议先校验查询结果再取首元素。Line [62]-Line [64] 直接取
[0],查询为空时会在postCreate阶段抛异常。建议先判空并给出明确错误信息。可参考的修改
postCreate { - inventory = JSONObjectUtil.rehashObject(querySdnController { - conditions=["uuid=${inventory.uuid}".toString()] - }[0], SdnControllerInventory.class) + def controllers = querySdnController { + conditions = ["uuid=${inventory.uuid}".toString()] + } + if (!controllers) { + throw new IllegalStateException("Failed to query created SDN controller by uuid: ${inventory.uuid}") + } + inventory = JSONObjectUtil.rehashObject(controllers[0], SdnControllerInventory.class) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy` around lines 62 - 64, 在使用 JSONObjectUtil.rehashObject(querySdnController { conditions=["uuid=${inventory.uuid}".toString()] }[0], SdnControllerInventory.class) 之前不要直接取索引 0;在 SdnControllerSpec 的 postCreate 流程里先检查 querySdnController 返回的结果是否为 null 或为空列表(例如检查变量或返回值的 size/empty),若为空则抛出或记录一个明确的错误(包含 inventory.uuid 和上下文信息),否则再取第一项并传入 JSONObjectUtil.rehashObject 转换为 SdnControllerInventory。header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java (1)
118-120: 建议抽取公共判定逻辑,避免 VO/Inventory 后续漂移。这里与
L3NetworkInventory.enableIpAddressAllocation()的分支几乎一致,建议下沉到L3NetworkType的静态工具方法统一复用。♻️ 参考改法
*** header/src/main/java/org/zstack/header/network/l3/L3NetworkType.java @@ + public static boolean isIpAddressAllocationEnabled(String typeName) { + if (!hasType(typeName)) { + return true; + } + return valueOf(typeName).isIpAddressAllocationEnabled(); + } *** header/src/main/java/org/zstack/header/network/l3/L3NetworkInventory.java @@ - if (L3NetworkType.hasType(getType())) { - return L3NetworkType.valueOf(getType()).isIpAddressAllocationEnabled(); - } - return true; + return L3NetworkType.isIpAddressAllocationEnabled(getType()); *** header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java @@ - if (L3NetworkType.hasType(getType())) { - return L3NetworkType.valueOf(getType()).isIpAddressAllocationEnabled(); - } - return true; + return L3NetworkType.isIpAddressAllocationEnabled(getType());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java` around lines 118 - 120, Extract the repeated type-checking logic into a single static helper on L3NetworkType and call it from both L3NetworkVO and L3NetworkInventory.enableIpAddressAllocation(): move the if (L3NetworkType.hasType(getType())) { return L3NetworkType.valueOf(getType()).isIpAddressAllocationEnabled(); } logic into a new L3NetworkType.static method (e.g., isIpAddressAllocationEnabled(String type) or isIpAddressAllocationEnabledFor(String type)) that performs the hasType check and returns the flag, then replace the existing branches in L3NetworkVO and L3NetworkInventory.enableIpAddressAllocation() to delegate to that new static method to avoid duplication and future drift.plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java (1)
4183-4184: 将 ZNS 端口常量提取,避免魔法值散落
"br-int"和"openvswitch"建议提取为类常量(或统一常量类),减少拼写漂移风险并提升可维护性。♻️ 建议修改
public class KVMHost extends HostBase implements Host { private static final CLogger logger = Utils.getLogger(KVMHost.class); private static final ZTester tester = Utils.getTester(); + private static final String ZNS_BRIDGE_NAME = "br-int"; + private static final String ZNS_BRIDGE_PORT_TYPE = "openvswitch";- to.setBridgeName("br-int"); - to.setBridgePortType("openvswitch"); + to.setBridgeName(ZNS_BRIDGE_NAME); + to.setBridgePortType(ZNS_BRIDGE_PORT_TYPE);As per coding guidelines: 避免使用魔法值(Magic Value),应替换为常量或枚举。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java` around lines 4183 - 4184, Replace the magic strings used when configuring ZNS ports by extracting them into constants: replace the literal "br-int" passed to to.setBridgeName and "openvswitch" passed to to.setBridgePortType with class-level constants (e.g., BRIDGE_NAME_INT and BRIDGE_PORT_TYPE_OPENVSWITCH) declared in KVMHost (or a shared constants class) and use those constant names in the calls to to.setBridgeName(...) and to.setBridgePortType(...); ensure the constants are public/static/final and documented so all usages reference the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java`:
- Around line 121-145: A VmNicVO is persisted by vnicFactory.createVmNic(nic,
spec) before callBeforeAllocateVmNicExtensions(...) but on extension failure the
code only records the error and doesn't remove that persisted nic, leaving an
orphaned VmNicVO/ResourceVO; in the Completion.fail(ErrorCode) branch delete the
persisted nicVO (and associated ResourceVO/any created system tags) inside a DB
transaction/SQLBatch (use dbf.remove(...) or equivalent) before adding the error
and calling wcomp.allDone(), ensuring the cleanup references the nicVO created
earlier so the rollback won't leave orphaned records.
In `@core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java`:
- Around line 101-109: The current submit method starts the timeout task via
thdf.submitTimeoutTask before putting the PendingEntry into pendingCalls, which
can race (timeout may run before put) and drop completion; fix by creating and
registering the PendingEntry in pendingCalls first (e.g., new
PendingEntry<>(completion, null) and put it), then call thdf.submitTimeoutTask
and store the returned TimeoutTaskReceipt into that PendingEntry (or replace the
entry with one containing the receipt). Update references: method submit, map
pendingCalls, class PendingEntry, thdf.submitTimeoutTask, fail/operr and
protocol.getCallbackPath() so the timeout handler will always find and remove
the registered entry.
In `@docs/modules/network/pages/networkResource/ZnsIntegration.adoc`:
- Around line 15-29: The example marked [source,go] is not valid Go: replace the
inline `###` comments with proper Go comments or remove them, add the missing
`struct` keyword for `type Segment` and ensure the slice field is declared
correctly (e.g., `CmsMetaDatas []Cms` with a proper struct tag like
`json:"cms"`), and make field names/types conform to Go syntax in the `Cms`
struct (e.g., ensure fields like `CmsUuid`, `Type`, `IP`, `Role`,
`CmsResourceUuid` use valid types and optional `json` tags); alternatively, if
you intend pseudocode, change the block language from `go` to something like
`text` and add a note that it’s pseudocode so readers aren’t misled.
In
`@header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java`:
- Around line 10-11: 为 AfterReleaseVmNicExtensionPoint 接口的 afterReleaseVmNic
方法添加方法级 Javadoc,说明回调契约:明确在何种时机框架会调用 afterReleaseVmNic,要求扩展实现必须在处理完成后且仅调用一次
Completion(调用 completion.success() 表示成功、调用 completion.fail(Throwable)
表示失败),不应阻塞调用线程或抛出未捕获异常,并说明是否允许异步完成及线程/事务约束;同时移除方法声明中多余的 public
修饰符以符合接口方法规范(定位符:AfterReleaseVmNicExtensionPoint.afterReleaseVmNic(VmNicInventory
nic, Completion completion))。
In `@network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java`:
- Line 197: 在 L3NetworkManagerImpl 中避免直接对 dbf.findByUuid(...) 的返回值调用
L3NetworkInventory.valueOf(...) 导致 NPE:先把 dbf.findByUuid(msg.getL3NetworkUuid(),
L3NetworkVO.class) 的结果赋给一个局部变量(例如 vo 或 l3Vo),对该变量判空;若为 null,调用
trigger.fail(...)(包含清晰的错误信息和 msg.getL3NetworkUuid())并返回;否则再调用
L3NetworkInventory.valueOf(...) 继续后续逻辑。
In `@plugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.java`:
- Around line 252-254: 当前在 KVMRealizeL2VlanNetworkBackend 的判断只为 dpdk
vhost-user(L2NetworkConstant.ACCEL_TYPE_VHOST_USER_SPACE)设置 srcPath,遗漏了
vDPA(L2NetworkConstant.ACCEL_TYPE_VDPA),导致 NIC 未被正确配置;在包含 to.setSrcPath(...)
的分支中扩展条件为同时匹配 ACCEL_TYPE_VHOST_USER_SPACE 或 ACCEL_TYPE_VDPA(检查 nic.getType()),使
to.setSrcPath 使用 L2NetworkConstant.OVN_DPDK_VNIC_SRC_PATH +
nic.getInternalName() 对两类加速 NIC 都生效;并在 KVMRealizeL2NoVlanNetworkBackend
中对称地做同样的修改以保持行为一致。
In
`@plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java`:
- Around line 273-279: The current try/catch in SdnControllerManagerImpl around
tagMgr.createTagsFromAPICreateMessage(...) and dbf.findByUuid(...) silently
swallows initialization failures and still calls publish(event); remove the
catch so failures propagate (or rethrow the exception) instead of continuing on
the success path, ensuring event.setInventory(...) and tag creation errors
prevent publish(event) from returning success; locate the block that calls
tagMgr.createTagsFromAPICreateMessage(...), dbf.findByUuid(...), and
event.setInventory(...) and delete the surrounding try/catch (or replace with a
rethrow) so errors are not suppressed.
- Around line 661-680: The loop building nicMaps in SdnControllerManagerImpl
(currently iterating nics and calling dbf.findByUuid for L3NetworkVO and
L2NetworkVO and reading L2_NETWORK_SDN_CONTROLLER_UUID tag per NIC) causes N+1
queries; refactor by adding a batch helper (e.g., buildNicToControllerMap or
resolveNicsToControllers) that: collects all l3 uuids from the nics, does a
single dbf.listByIds/findByUuids for L3NetworkVO, collects all referenced l2
uuids and batch-fetches L2NetworkVOs, then resolves controllerUuid tokens for
those L2s in one pass and returns a Map<controllerUuid, List<VmNicInventory>>;
replace the three similar loops (around the shown block and at ranges 698-717,
734-753) to call this helper and handle null/missing controllerUuid by failing
the completion once with the same operr message referencing l2 uuid.
- Around line 663-670: The loop that looks up L3NetworkVO and L2NetworkVO should
fail-fast instead of silently continuing: when
dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class) or
dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class) returns null, stop
processing and fail the allocation (e.g., call completion.fail or throw an
exception) with a clear error message including the missing UUID and NIC info;
do not let nicMaps remain empty and return completion.success(). Update the
logic around shouldSkipSdnForNic to still allow skipping when intentional, but
treat null VO results as fatal data-inconsistency errors and surface them
immediately.
In `@rest/src/main/resources/scripts/RestDocumentationGenerator.groovy`:
- Around line 2846-2849: The loop currently only catches CloudRuntimeException
when calling checkMD(mdPath, it.value), so other runtime exceptions (e.g.,
IndexOutOfBoundsException, NullPointerException thrown from checkMD or
getExistGlobalConfigMarkDown) will break the loop and prevent collecting all
errors; update the try/catch to catch Exception (or RuntimeException) in
addition to CloudRuntimeException around the call sites (checkMD and any calls
to getExistGlobalConfigMarkDown) and add their messages to allErrors (preserving
existing CloudRuntimeException behavior) so the loop continues and aggregates
every markdown validation failure.
- Around line 851-861: 当前校验用的是 markDown.desc_CN.isEmpty() 等判断,但
GlobalConfigMarkDown 是从 new GlobalConfigMarkDown() 开始解析,默认值为 "PLACEHOLDER...",所以
.isEmpty() 会漏报未出现的 section。更新校验逻辑(在 RestDocumentationGenerator.groovy 中处理
getExistGlobalConfigMarkDown 返回的 GlobalConfigMarkDown)改为使用一个辅助判空方法(例如
isMissingField(String s))并在该方法中同时判断 null、trim().isEmpty() 和是否以
"PLACEHOLDER"(或项目中实际的占位符前缀)开头,替换对
desc_CN、name_CN、valueRangeRemark、defaultValueRemark、resourcesGranularitiesRemark、additionalRemark、backgroundInformation、isUIExposed、isCLIExposed
的所有 .isEmpty() 调用;或者相应地修改 getExistGlobalConfigMarkDown 使未在 markdown 中出现的字段返回
null,并据此做相同的判定。
---
Nitpick comments:
In `@header/src/main/java/org/zstack/header/network/l3/L3NetworkVO.java`:
- Around line 118-120: Extract the repeated type-checking logic into a single
static helper on L3NetworkType and call it from both L3NetworkVO and
L3NetworkInventory.enableIpAddressAllocation(): move the if
(L3NetworkType.hasType(getType())) { return
L3NetworkType.valueOf(getType()).isIpAddressAllocationEnabled(); } logic into a
new L3NetworkType.static method (e.g., isIpAddressAllocationEnabled(String type)
or isIpAddressAllocationEnabledFor(String type)) that performs the hasType check
and returns the flag, then replace the existing branches in L3NetworkVO and
L3NetworkInventory.enableIpAddressAllocation() to delegate to that new static
method to avoid duplication and future drift.
In
`@header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java`:
- Around line 6-11: The interface AfterReleaseVmNicExtensionPoint and its method
afterReleaseVmNic currently mix "Release" in the name with "deleted from
database" in the Javadoc; update either the name or the comment so lifecycle
semantics match: either rename the interface/method to
DeletedVmNicExtensionPoint/afterDeletedVmNic if it is invoked after DB deletion,
or change the Javadoc to state the exact "release" stage (e.g., resource
release/cleanup before/after DB deletion) if the name is correct; make the
change where AfterReleaseVmNicExtensionPoint and afterReleaseVmNic are declared
and ensure VmNicInventory references and any callers/registrations are updated
consistently to avoid mismatched lifecycle semantics.
In `@plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java`:
- Around line 4183-4184: Replace the magic strings used when configuring ZNS
ports by extracting them into constants: replace the literal "br-int" passed to
to.setBridgeName and "openvswitch" passed to to.setBridgePortType with
class-level constants (e.g., BRIDGE_NAME_INT and BRIDGE_PORT_TYPE_OPENVSWITCH)
declared in KVMHost (or a shared constants class) and use those constant names
in the calls to to.setBridgeName(...) and to.setBridgePortType(...); ensure the
constants are public/static/final and documented so all usages reference the
single source of truth.
In `@testlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovy`:
- Around line 62-64: 在使用 JSONObjectUtil.rehashObject(querySdnController {
conditions=["uuid=${inventory.uuid}".toString()] }[0],
SdnControllerInventory.class) 之前不要直接取索引 0;在 SdnControllerSpec 的 postCreate
流程里先检查 querySdnController 返回的结果是否为 null 或为空列表(例如检查变量或返回值的
size/empty),若为空则抛出或记录一个明确的错误(包含 inventory.uuid 和上下文信息),否则再取第一项并传入
JSONObjectUtil.rehashObject 转换为 SdnControllerInventory。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)
Review profile: CHILL
Plan: Pro
Run ID: 190b27bf-f5cc-42a9-9b21-26c5d0efaf60
⛔ Files ignored due to path filters (9)
build/pom.xmlis excluded by!**/*.xmlconf/springConfigXml/VmInstanceManager.xmlis excluded by!**/*.xmlconf/springConfigXml/sdnController.xmlis excluded by!**/*.xmlsdk/src/main/java/SourceClassMap.javais excluded by!sdk/**sdk/src/main/java/org/zstack/sdk/CreateL2GeneveNetworkAction.javais excluded by!sdk/**sdk/src/main/java/org/zstack/sdk/L2GeneveNetworkInventory.javais excluded by!sdk/**sdk/src/main/java/org/zstack/sdk/NicTO.javais excluded by!sdk/**sdk/src/main/java/org/zstack/sdk/ZnsControllerInventory.javais excluded by!sdk/**sdk/src/main/java/org/zstack/sdk/ZnsTransportZoneInventory.javais excluded by!sdk/**
📒 Files selected for processing (40)
compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmAllocateSdnNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmInstanceBase.javacompute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.javacompute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmSystemTags.javaconf/db/upgrade/V5.5.18__schema.sqlcore/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.javacore/src/main/java/org/zstack/core/rest/webhook/WebhookProtocol.javadocs/modules/network/nav.adocdocs/modules/network/pages/networkResource/ZStackL2NetworkType.adocdocs/modules/network/pages/networkResource/ZnsIntegration.adocdocs/modules/network/pages/networkResource/networkResource.adocheader/src/main/java/org/zstack/header/network/l2/L2NetworkConstant.javaheader/src/main/java/org/zstack/header/network/l3/AfterSetL3NetworkMtuExtensionPoint.javaheader/src/main/java/org/zstack/header/network/l3/L3NetworkInventory.javaheader/src/main/java/org/zstack/header/network/l3/L3NetworkType.javaheader/src/main/java/org/zstack/header/network/l3/L3NetworkVO.javaheader/src/main/java/org/zstack/header/vm/AfterAllocateSdnNicExtensionPoint.javaheader/src/main/java/org/zstack/header/vm/AfterAllocateVmNicIpExtensionPoint.javaheader/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.javaheader/src/main/java/org/zstack/header/vm/BeforeAllocateVmNicExtensionPoint.javaheader/src/main/java/org/zstack/header/vm/VmInstanceNicFactory.javaheader/src/main/java/org/zstack/header/vm/VmOvsNicConstant.javanetwork/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.javaplugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.javaplugin/kvm/src/main/java/org/zstack/kvm/KVMHost.javaplugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.javaplugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2NoVlanNetworkBackend.javaplugin/kvm/src/main/java/org/zstack/kvm/KVMRealizeL2VlanNetworkBackend.javaplugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerBase.javaplugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerFactory.javaplugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.javaplugin/sdnController/src/main/java/org/zstack/sdnController/h3cVcfc/H3cVcfcSdnControllerFactory.javaplugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/controller/SugonSdnControllerFactory.javarest/src/main/resources/scripts/RestDocumentationGenerator.groovytestlib/src/main/java/org/zstack/testlib/ApiHelper.groovytestlib/src/main/java/org/zstack/testlib/SdnControllerSpec.groovyutils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
💤 Files with no reviewable changes (4)
- plugin/sdnController/src/main/java/org/zstack/sdnController/h3cVcfc/H3cVcfcSdnControllerFactory.java
- plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerFactory.java
- header/src/main/java/org/zstack/header/vm/VmOvsNicConstant.java
- plugin/sugonSdnController/src/main/java/org/zstack/sugonSdnController/controller/SugonSdnControllerFactory.java
| // Persist VmNicVO first so that ResourceVO entry exists before extensions | ||
| // (e.g. SDN controllers) attempt to create SystemTags referencing the NIC UUID. | ||
| VmNicVO nicVO = vnicFactory.createVmNic(nic, spec); | ||
|
|
||
| nic.setDeviceId(deviceId); | ||
| nic.setInternalName(VmNicVO.generateNicInternalName(spec.getVmInventory().getInternalId(), nic.getDeviceId())); | ||
| nic.setState(disableL3Networks.contains(nic.getL3NetworkUuid()) ? VmNicState.disable.toString() : VmNicState.enable.toString()); | ||
| new SQLBatch() { | ||
| callBeforeAllocateVmNicExtensions(nic, spec, new Completion(wcomp) { | ||
| @Override | ||
| protected void scripts() { | ||
| VmNicVO nicVO = vnicFactory.createVmNic(nic, spec); | ||
| if (!nw.enableIpAddressAllocation() && nicNetworkInfoMap != null | ||
| && nicNetworkInfoMap.containsKey(nw.getUuid()) | ||
| && spec.getVmInventory().getType().equals(VmInstanceConstant.USER_VM_TYPE)) { | ||
| NicIpAddressInfo nicNicIpAddressInfo = nicNetworkInfoMap.get(nic.getL3NetworkUuid()); | ||
| if (!nicNicIpAddressInfo.ipv6Address.isEmpty()) { | ||
| UsedIpVO vo = new UsedIpVO(); | ||
| vo.setUuid(Platform.getUuid()); | ||
| vo.setIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(nicNicIpAddressInfo.ipv6Address)); | ||
| vo.setNetmask(IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(nicNicIpAddressInfo.ipv6Address+"/"+ nicNicIpAddressInfo.ipv6Prefix)); | ||
| vo.setGateway(nicNicIpAddressInfo.ipv6Gateway.isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(nicNicIpAddressInfo.ipv6Gateway)); | ||
| vo.setIpVersion(IPv6Constants.IPv6); | ||
| vo.setVmNicUuid(nic.getUuid()); | ||
| vo.setL3NetworkUuid(nic.getL3NetworkUuid()); | ||
| vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); | ||
| vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(nic.getL3NetworkUuid(), vo.getIp())); | ||
| nic.setUsedIpUuid(vo.getUuid()); | ||
| nicVO.setUsedIpUuid(vo.getUuid()); | ||
| nicVO.setIp(vo.getIp()); | ||
| nicVO.setNetmask(vo.getNetmask()); | ||
| nicVO.setGateway(vo.getGateway()); | ||
| dbf.persist(vo); | ||
| public void success() { | ||
| new SQLBatch() { | ||
| @Override | ||
| protected void scripts() { | ||
| persistStaticIpIfNeeded(nic, nicVO, nw, nicNetworkInfoMap, spec); | ||
| nics.add(nic); | ||
| VmNicVO updated = dbf.updateAndRefresh(nicVO); | ||
| addVmNicConfig(updated, spec, nicSpec); | ||
| } | ||
| if (!nicNicIpAddressInfo.ipv4Address.isEmpty()) { | ||
| UsedIpVO vo = new UsedIpVO(); | ||
| vo.setUuid(Platform.getUuid()); | ||
| vo.setIp(nicNicIpAddressInfo.ipv4Address); | ||
| vo.setGateway(nicNicIpAddressInfo.ipv4Gateway); | ||
| vo.setNetmask(nicNicIpAddressInfo.ipv4Netmask); | ||
| vo.setIpVersion(IPv6Constants.IPv4); | ||
| vo.setVmNicUuid(nic.getUuid()); | ||
| vo.setL3NetworkUuid(nic.getL3NetworkUuid()); | ||
| vo.setIpInLong(NetworkUtils.ipv4StringToLong(vo.getIp())); | ||
| vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); | ||
| vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(nic.getL3NetworkUuid(), vo.getIp())); | ||
| nic.setUsedIpUuid(vo.getUuid()); | ||
| nicVO.setUsedIpUuid(vo.getUuid()); | ||
| nicVO.setIp(vo.getIp()); | ||
| nicVO.setNetmask(vo.getNetmask()); | ||
| nicVO.setGateway(vo.getGateway()); | ||
| dbf.persist(vo); | ||
| } | ||
| } | ||
| nics.add(nic); | ||
| nicVO = dbf.updateAndRefresh(nicVO); | ||
| addVmNicConfig(nicVO, spec, nicSpec); | ||
| }.execute(); | ||
| wcomp.done(); | ||
| } | ||
| }.execute(); | ||
| wcomp.done(); | ||
|
|
||
| @Override | ||
| public void fail(ErrorCode errorCode) { | ||
| errs.add(errorCode); | ||
| wcomp.allDone(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
在扩展失败时清理已落库的 VmNicVO。
Line 123 已经持久化了 VmNicVO,但只有 Line 132 成功分支才把这张 NIC 放进后续集合;Line 141-144 失败时只是记错并结束。当前回滚逻辑会遍历 spec.getDestNics(),它看不到这张尚未加入集合的 NIC,因此这里会遗留孤儿 VmNicVO/ResourceVO。
🛠️ 一个直接的修复方向
callBeforeAllocateVmNicExtensions(nic, spec, new Completion(wcomp) {
`@Override`
public void success() {
new SQLBatch() {
@@
`@Override`
public void fail(ErrorCode errorCode) {
+ vnicFactory.releaseVmNic(nic);
errs.add(errorCode);
wcomp.allDone();
}
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@compute/src/main/java/org/zstack/compute/vm/VmAllocateNicFlow.java` around
lines 121 - 145, A VmNicVO is persisted by vnicFactory.createVmNic(nic, spec)
before callBeforeAllocateVmNicExtensions(...) but on extension failure the code
only records the error and doesn't remove that persisted nic, leaving an
orphaned VmNicVO/ResourceVO; in the Completion.fail(ErrorCode) branch delete the
persisted nicVO (and associated ResourceVO/any created system tags) inside a DB
transaction/SQLBatch (use dbf.remove(...) or equivalent) before adding the error
and calling wcomp.allDone(), ensuring the cleanup references the nicVO created
earlier so the rollback won't leave orphaned records.
| public String submit(ReturnValueCompletion<T> completion, TimeUnit unit, long timeout) { | ||
| String taskId = Platform.getUuid(); | ||
|
|
||
| ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt = thdf.submitTimeoutTask(() -> { | ||
| fail(taskId, operr("[Webhook Timeout] callback timed out for taskId[%s], path[%s]", | ||
| taskId, protocol.getCallbackPath())); | ||
| }, unit, timeout); | ||
|
|
||
| pendingCalls.put(taskId, new PendingEntry<>(completion, timeoutReceipt)); |
There was a problem hiding this comment.
先注册 pending entry,再启动超时任务。
Line 104 在 Line 109 之前启动 timeout;如果 timeout == 0,或者调度线程立刻执行,fail(taskId, ...) 会先跑到 pendingCalls.remove(taskId) 并直接返回。随后 Line 109 再 put,这次调用就会变成一个永远不会超时、也不会完成的悬挂请求。
🛠️ 一个可行的修复模式
private static class PendingEntry<T> {
final ReturnValueCompletion<T> completion;
- final ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt;
+ volatile ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt;
- PendingEntry(ReturnValueCompletion<T> completion,
- ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt) {
+ PendingEntry(ReturnValueCompletion<T> completion) {
this.completion = completion;
- this.timeoutReceipt = timeoutReceipt;
}
}
@@
public String submit(ReturnValueCompletion<T> completion, TimeUnit unit, long timeout) {
String taskId = Platform.getUuid();
-
- ThreadFacadeImpl.TimeoutTaskReceipt timeoutReceipt = thdf.submitTimeoutTask(() -> {
- fail(taskId, operr("[Webhook Timeout] callback timed out for taskId[%s], path[%s]",
- taskId, protocol.getCallbackPath()));
- }, unit, timeout);
-
- pendingCalls.put(taskId, new PendingEntry<>(completion, timeoutReceipt));
+ PendingEntry<T> entry = new PendingEntry<>(completion);
+ pendingCalls.put(taskId, entry);
+ entry.timeoutReceipt = thdf.submitTimeoutTask(() -> {
+ fail(taskId, operr("[Webhook Timeout] callback timed out for taskId[%s], path[%s]",
+ taskId, protocol.getCallbackPath()));
+ }, unit, timeout);
return taskId;
}
@@
PendingEntry<T> entry = pendingCalls.remove(taskId);
if (entry != null) {
- entry.timeoutReceipt.cancel();
+ if (entry.timeoutReceipt != null) {
+ entry.timeoutReceipt.cancel();
+ }
entry.completion.fail(error);
}
@@
PendingEntry<T> entry = pendingCalls.remove(taskId);
if (entry == null) {
@@
- entry.timeoutReceipt.cancel();
+ if (entry.timeoutReceipt != null) {
+ entry.timeoutReceipt.cancel();
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/src/main/java/org/zstack/core/rest/webhook/WebhookCallbackClient.java`
around lines 101 - 109, The current submit method starts the timeout task via
thdf.submitTimeoutTask before putting the PendingEntry into pendingCalls, which
can race (timeout may run before put) and drop completion; fix by creating and
registering the PendingEntry in pendingCalls first (e.g., new
PendingEntry<>(completion, null) and put it), then call thdf.submitTimeoutTask
and store the returned TimeoutTaskReceipt into that PendingEntry (or replace the
entry with one containing the receipt). Update references: method submit, map
pendingCalls, class PendingEntry, thdf.submitTimeoutTask, fail/operr and
protocol.getCallbackPath() so the timeout handler will always find and remove
the registered entry.
| [source,go] | ||
| ---- | ||
| type Cms struct { | ||
| CmsUuid string | ||
| Type string ### cloud/zsv/zaku/zns | ||
| IP string ### cloud mn vip | ||
| Role string ###owner, user | ||
| CmsResourceUuid string ###owner, user | ||
| } | ||
|
|
||
| type Segment { | ||
| ... | ||
| CmsMetaDatas []Cms `json:"cms"` | ||
| } | ||
| ---- |
There was a problem hiding this comment.
把这段示例改成合法 Go,或者明确标成伪代码。
这里标了 [source,go],但片段本身不是有效 Go:### 不是 Go 注释,type Segment { 也缺少 struct。这会误导读者直接复制示例,也会让语法高亮给出错误暗示。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/modules/network/pages/networkResource/ZnsIntegration.adoc` around lines
15 - 29, The example marked [source,go] is not valid Go: replace the inline
`###` comments with proper Go comments or remove them, add the missing `struct`
keyword for `type Segment` and ensure the slice field is declared correctly
(e.g., `CmsMetaDatas []Cms` with a proper struct tag like `json:"cms"`), and
make field names/types conform to Go syntax in the `Cms` struct (e.g., ensure
fields like `CmsUuid`, `Type`, `IP`, `Role`, `CmsResourceUuid` use valid types
and optional `json` tags); alternatively, if you intend pseudocode, change the
block language from `go` to something like `text` and add a note that it’s
pseudocode so readers aren’t misled.
| public interface AfterReleaseVmNicExtensionPoint { | ||
| void afterReleaseVmNic(VmNicInventory nic, Completion completion); |
There was a problem hiding this comment.
请为接口方法补充方法级 Javadoc,明确回调契约。
当前只有接口级说明,Completion 的调用约束(例如成功/失败分支和调用时机)不够明确,扩展实现方容易误用。
建议补丁
public interface AfterReleaseVmNicExtensionPoint {
+ /**
+ * Called after VmNic reaches this extension point stage.
+ * Implementations must complete via {`@code` completion.success()} or {`@code` completion.fail()}.
+ */
void afterReleaseVmNic(VmNicInventory nic, Completion completion);
}As per coding guidelines, “接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释”。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public interface AfterReleaseVmNicExtensionPoint { | |
| void afterReleaseVmNic(VmNicInventory nic, Completion completion); | |
| public interface AfterReleaseVmNicExtensionPoint { | |
| /** | |
| * Called after VmNic reaches this extension point stage. | |
| * Implementations must complete via {`@code` completion.success()} or {`@code` completion.fail()}. | |
| */ | |
| void afterReleaseVmNic(VmNicInventory nic, Completion completion); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@header/src/main/java/org/zstack/header/vm/AfterReleaseVmNicExtensionPoint.java`
around lines 10 - 11, 为 AfterReleaseVmNicExtensionPoint 接口的 afterReleaseVmNic
方法添加方法级 Javadoc,说明回调契约:明确在何种时机框架会调用 afterReleaseVmNic,要求扩展实现必须在处理完成后且仅调用一次
Completion(调用 completion.success() 表示成功、调用 completion.fail(Throwable)
表示失败),不应阻塞调用线程或抛出未捕获异常,并说明是否允许异步完成及线程/事务约束;同时移除方法声明中多余的 public
修饰符以符合接口方法规范(定位符:AfterReleaseVmNicExtensionPoint.afterReleaseVmNic(VmNicInventory
nic, Completion completion))。
| return; | ||
| } | ||
|
|
||
| L3NetworkInventory l3Inv = L3NetworkInventory.valueOf(dbf.findByUuid(msg.getL3NetworkUuid(), L3NetworkVO.class)); |
There was a problem hiding this comment.
请为 L3 查询结果增加空值保护,避免 NPE。
Line 197 直接对 dbf.findByUuid(...) 结果做 L3NetworkInventory.valueOf(...),在并发删除等场景下可能触发空指针并返回不明确错误。建议先判空并显式 trigger.fail(...)。
💡建议修改
- L3NetworkInventory l3Inv = L3NetworkInventory.valueOf(dbf.findByUuid(msg.getL3NetworkUuid(), L3NetworkVO.class));
+ L3NetworkVO l3Vo = dbf.findByUuid(msg.getL3NetworkUuid(), L3NetworkVO.class);
+ if (l3Vo == null) {
+ trigger.fail(operr("cannot find l3Network[uuid:%s]", msg.getL3NetworkUuid()));
+ return;
+ }
+ L3NetworkInventory l3Inv = L3NetworkInventory.valueOf(l3Vo);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java` at
line 197, 在 L3NetworkManagerImpl 中避免直接对 dbf.findByUuid(...) 的返回值调用
L3NetworkInventory.valueOf(...) 导致 NPE:先把 dbf.findByUuid(msg.getL3NetworkUuid(),
L3NetworkVO.class) 的结果赋给一个局部变量(例如 vo 或 l3Vo),对该变量判空;若为 null,调用
trigger.fail(...)(包含清晰的错误信息和 msg.getL3NetworkUuid())并返回;否则再调用
L3NetworkInventory.valueOf(...) 继续后续逻辑。
| try { | ||
| tagMgr.createTagsFromAPICreateMessage(msg, vo.getUuid(), SdnControllerVO.class.getSimpleName()); | ||
| event.setInventory(SdnControllerInventory.valueOf(dbf.findByUuid(vo.getUuid(), SdnControllerVO.class))); | ||
| } catch (Exception e) { | ||
| logger.warn(String.format("failed to load SdnControllerVO[uuid:%s] after init: %s", | ||
| vo.getUuid(), e.getMessage()), e); | ||
| } |
There was a problem hiding this comment.
不要在成功路径里吞掉初始化失败。
Line 274 或 Line 275 任一失败,这里都会继续 publish(event),调用方仍会收到 success,但 tag 可能没建、inventory 也可能为空。这个分支会把“创建成功”变成静默的部分成功。
Based on learnings 在 ZStack 中,资源创建成功路径的 tagMgr.createTagsFromAPICreateMessage(...) 和后续 dbf.findByUuid(...) 不需要用 try/catch 来防御“资源不存在”异常,应依赖生命周期和数据库约束保证一致性。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java`
around lines 273 - 279, The current try/catch in SdnControllerManagerImpl around
tagMgr.createTagsFromAPICreateMessage(...) and dbf.findByUuid(...) silently
swallows initialization failures and still calls publish(event); remove the
catch so failures propagate (or rethrow the exception) instead of continuing on
the success path, ensuring event.setInventory(...) and tag creation errors
prevent publish(event) from returning success; locate the block that calls
tagMgr.createTagsFromAPICreateMessage(...), dbf.findByUuid(...), and
event.setInventory(...) and delete the surrounding try/catch (or replace with a
rethrow) so errors are not suppressed.
| Map<String, List<VmNicInventory>> nicMaps = new HashMap<>(); | ||
| for (VmNicInventory nic : nics) { | ||
| L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); | ||
| if (l3Vo == null) { | ||
| continue; | ||
| } | ||
|
|
||
| L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); | ||
| if (l2VO == null || shouldSkipSdnForNic(l2VO)) { | ||
| continue; | ||
| } | ||
|
|
||
| String controllerUuid = L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID.getTokenByResourceUuid( | ||
| l2VO.getUuid(), L2NetworkSystemTags.L2_NETWORK_SDN_CONTROLLER_UUID_TOKEN); | ||
| if (controllerUuid == null) { | ||
| completion.fail(operr(ORG_ZSTACK_SDNCONTROLLER_10006, "sdn l2 network[uuid:%s] has not attached controller", l2VO.getUuid())); | ||
| return; | ||
| } | ||
|
|
||
| nicMaps.computeIfAbsent(controllerUuid, k -> new ArrayList<>()).add(nic); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
把 NIC→controller 解析提成一个批量 helper。
这三段逻辑对每个 NIC 都做两次 findByUuid(),而且几乎完全复制了三遍;NIC 数量一多就会退化成明显的 N+1 查询。建议先批量取 L3NetworkVO / L2NetworkVO / controller tag 映射,再复用一个公共 helper 生成 nicMaps。
As per coding guidelines “禁止循环里套查询,避免嵌套查询带来的性能问题”。
Also applies to: 698-717, 734-753
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java`
around lines 661 - 680, The loop building nicMaps in SdnControllerManagerImpl
(currently iterating nics and calling dbf.findByUuid for L3NetworkVO and
L2NetworkVO and reading L2_NETWORK_SDN_CONTROLLER_UUID tag per NIC) causes N+1
queries; refactor by adding a batch helper (e.g., buildNicToControllerMap or
resolveNicsToControllers) that: collects all l3 uuids from the nics, does a
single dbf.listByIds/findByUuids for L3NetworkVO, collects all referenced l2
uuids and batch-fetches L2NetworkVOs, then resolves controllerUuid tokens for
those L2s in one pass and returns a Map<controllerUuid, List<VmNicInventory>>;
replace the three similar loops (around the shown block and at ranges 698-717,
734-753) to call this helper and handle null/missing controllerUuid by failing
the completion once with the same operr message referencing l2 uuid.
| L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class); | ||
| if (l3Vo == null) { | ||
| continue; | ||
| } | ||
|
|
||
| L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class); | ||
| if (l2VO == null || shouldSkipSdnForNic(l2VO)) { | ||
| continue; |
There was a problem hiding this comment.
分配路径不应静默跳过缺失的网络对象。
这里对缺失的 L3NetworkVO / L2NetworkVO 直接 continue,最终 nicMaps 可能为空并返回 completion.success()。结果就是 VM 流程继续成功,但 SDN 端口根本没有创建。分配路径应该 fail-fast,而不是把这种数据不一致当成可忽略情况。
🛠️ 一个更安全的处理方式
for (VmNicInventory nic : nics) {
L3NetworkVO l3Vo = dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class);
if (l3Vo == null) {
- continue;
+ completion.fail(operr("cannot find l3Network[uuid:%s] for vmNic[uuid:%s]",
+ nic.getL3NetworkUuid(), nic.getUuid()));
+ return;
}
L2NetworkVO l2VO = dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class);
- if (l2VO == null || shouldSkipSdnForNic(l2VO)) {
- continue;
+ if (l2VO == null) {
+ completion.fail(operr("cannot find l2Network[uuid:%s] for vmNic[uuid:%s]",
+ l3Vo.getL2NetworkUuid(), nic.getUuid()));
+ return;
+ }
+ if (shouldSkipSdnForNic(l2VO)) {
+ continue;
}Based on learnings backend realization code can safely assume the existence of the VO for the resource being operated on due to lifecycle management and strong foreign key constraints in the database schema.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@plugin/sdnController/src/main/java/org/zstack/sdnController/SdnControllerManagerImpl.java`
around lines 663 - 670, The loop that looks up L3NetworkVO and L2NetworkVO
should fail-fast instead of silently continuing: when
dbf.findByUuid(nic.getL3NetworkUuid(), L3NetworkVO.class) or
dbf.findByUuid(l3Vo.getL2NetworkUuid(), L2NetworkVO.class) returns null, stop
processing and fail the allocation (e.g., call completion.fail or throw an
exception) with a clear error message including the missing UUID and NIC info;
do not let nicMaps remain empty and return completion.success(). Update the
logic around shouldSkipSdnForNic to still allow skipping when intentional, but
treat null VO results as fatal data-inconsistency errors and surface them
immediately.
| GlobalConfigMarkDown markDown = getExistGlobalConfigMarkDown(mdPath) | ||
| List<String> missingFields = [] | ||
| if (markDown.desc_CN.isEmpty()) missingFields.add("desc_CN") | ||
| if (markDown.name_CN.isEmpty()) missingFields.add("name_CN") | ||
| if (markDown.valueRangeRemark.isEmpty()) missingFields.add("valueRangeRemark") | ||
| if (markDown.defaultValueRemark.isEmpty()) missingFields.add("defaultValueRemark") | ||
| if (markDown.resourcesGranularitiesRemark.isEmpty()) missingFields.add("resourcesGranularitiesRemark") | ||
| if (markDown.additionalRemark.isEmpty()) missingFields.add("additionalRemark") | ||
| if (markDown.backgroundInformation.isEmpty()) missingFields.add("backgroundInformation") | ||
| if (markDown.isUIExposed.isEmpty()) missingFields.add("isUIExposed") | ||
| if (markDown.isCLIExposed.isEmpty()) missingFields.add("isCLIExposed") |
There was a problem hiding this comment.
缺失章节会被误判为已填写
这里用 .isEmpty() 判断必填项会漏报整段缺失的 section。getExistGlobalConfigMarkDown() 是从 new GlobalConfigMarkDown() 开始解析的,这些字段默认都是非空的 PLACEHOLDER...;如果 markdown 里根本没有对应标题,解析后字段仍然不是空串,所以当前校验会直接通过。
建议修复
GlobalConfigMarkDown markDown = getExistGlobalConfigMarkDown(mdPath)
List<String> missingFields = []
- if (markDown.desc_CN.isEmpty()) missingFields.add("desc_CN")
- if (markDown.name_CN.isEmpty()) missingFields.add("name_CN")
- if (markDown.valueRangeRemark.isEmpty()) missingFields.add("valueRangeRemark")
- if (markDown.defaultValueRemark.isEmpty()) missingFields.add("defaultValueRemark")
- if (markDown.resourcesGranularitiesRemark.isEmpty()) missingFields.add("resourcesGranularitiesRemark")
- if (markDown.additionalRemark.isEmpty()) missingFields.add("additionalRemark")
- if (markDown.backgroundInformation.isEmpty()) missingFields.add("backgroundInformation")
- if (markDown.isUIExposed.isEmpty()) missingFields.add("isUIExposed")
- if (markDown.isCLIExposed.isEmpty()) missingFields.add("isCLIExposed")
+ def missing = { String value ->
+ StringUtils.isBlank(value) || value.startsWith(PLACEHOLDER)
+ }
+ if (missing(markDown.desc_CN)) missingFields.add("desc_CN")
+ if (missing(markDown.name_CN)) missingFields.add("name_CN")
+ if (missing(markDown.valueRangeRemark)) missingFields.add("valueRangeRemark")
+ if (missing(markDown.defaultValueRemark)) missingFields.add("defaultValueRemark")
+ if (missing(markDown.resourcesGranularitiesRemark)) missingFields.add("resourcesGranularitiesRemark")
+ if (missing(markDown.additionalRemark)) missingFields.add("additionalRemark")
+ if (missing(markDown.backgroundInformation)) missingFields.add("backgroundInformation")
+ if (missing(markDown.isUIExposed)) missingFields.add("isUIExposed")
+ if (missing(markDown.isCLIExposed)) missingFields.add("isCLIExposed")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rest/src/main/resources/scripts/RestDocumentationGenerator.groovy` around
lines 851 - 861, 当前校验用的是 markDown.desc_CN.isEmpty() 等判断,但 GlobalConfigMarkDown
是从 new GlobalConfigMarkDown() 开始解析,默认值为 "PLACEHOLDER...",所以 .isEmpty() 会漏报未出现的
section。更新校验逻辑(在 RestDocumentationGenerator.groovy 中处理
getExistGlobalConfigMarkDown 返回的 GlobalConfigMarkDown)改为使用一个辅助判空方法(例如
isMissingField(String s))并在该方法中同时判断 null、trim().isEmpty() 和是否以
"PLACEHOLDER"(或项目中实际的占位符前缀)开头,替换对
desc_CN、name_CN、valueRangeRemark、defaultValueRemark、resourcesGranularitiesRemark、additionalRemark、backgroundInformation、isUIExposed、isCLIExposed
的所有 .isEmpty() 调用;或者相应地修改 getExistGlobalConfigMarkDown 使未在 markdown 中出现的字段返回
null,并据此做相同的判定。
| try { | ||
| checkMD(mdPath, it.value) | ||
| } catch (CloudRuntimeException e) { | ||
| allErrors.add(e.message) |
There was a problem hiding this comment.
这里只聚合了 CloudRuntimeException,仍然会在首个解析异常处提前退出
新逻辑的目标是汇总所有 markdown 校验失败,但 checkMD() / getExistGlobalConfigMarkDown() 里仍可能抛出普通 RuntimeException(例如 markdown 结构异常触发的 IndexOutOfBoundsException 或 NullPointerException)。当前只 catch CloudRuntimeException,这类文件一旦出现,循环还是会直接中断,最终拿不到完整的错误列表。
建议修复
try {
checkMD(mdPath, it.value)
- } catch (CloudRuntimeException e) {
- allErrors.add(e.message)
+ } catch (Exception e) {
+ allErrors.add(e.message ?: "Validation failed for ${mdPath}: ${e.class.name}")
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rest/src/main/resources/scripts/RestDocumentationGenerator.groovy` around
lines 2846 - 2849, The loop currently only catches CloudRuntimeException when
calling checkMD(mdPath, it.value), so other runtime exceptions (e.g.,
IndexOutOfBoundsException, NullPointerException thrown from checkMD or
getExistGlobalConfigMarkDown) will break the loop and prevent collecting all
errors; update the try/catch to catch Exception (or RuntimeException) in
addition to CloudRuntimeException around the call sites (checkMD and any calls
to getExistGlobalConfigMarkDown) and add their messages to allErrors (preserving
existing CloudRuntimeException behavior) so the loop continues and aggregates
every markdown validation failure.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java (1)
722-739: 这里建议别再新增一段TypedQuery查询分支。这段和上面的 L3 分支结构几乎一致,但继续引入 JPQL 字符串会让字段变更更脆弱,也把相同的筛选条件又复制了一遍。更合适的是抽一个按网络范围查 VM 的 helper,并改用现有的
Q.New(...)/SQL.New(...)风格收敛实现。As per coding guidelines 新增代码里面避免使用
TypedQuery。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java` around lines 722 - 739, The new anonymous Callable in VmCascadeExtension introduces a raw TypedQuery JPQL branch to fetch VMs by L2 network UUIDs; extract this into a shared helper (e.g., queryVmsByNetworkRange or findVmsByL2NetworkUuids) and reimplement it using the project-standard Q.New(...) / SQL.New(...) API used by the existing L3 branch, reusing the same filters (vm.type == VmInstanceConstant.USER_VM_TYPE, vm.state in {Stopped, Paused, Running, Destroyed}, join through VmNicVO -> L3NetworkVO -> L2Network UUIDs) instead of adding another TypedQuery; update the Callable to call that helper and remove the JPQL string so the code follows the coding guideline of avoiding TypedQuery.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java`:
- Around line 339-343: The current L2 delete flow first derives l3Uuids via
Q.New(L3NetworkVO).in(L3NetworkVO_.l2NetworkUuid, l2Uuids) and later re-queries
NIC/VMs using nic.l3NetworkUuid, but VmNicVO.l3NetworkUuid is SET_NULL on L3
delete so NICs can be missed if L3s were processed earlier; instead, compute the
affected VmNic/VM set once up-front for the L2 cascade (e.g. query VmNicVO by
the L2 identifier(s) or by joined criteria that do not rely on L3NetworkVO
existence), store that NIC/VM list (used where msgs is built) and reuse it in
subsequent steps instead of re-querying via L3NetworkVO/l3.uuid so the detach
logic always runs for those NICs/VMs.
---
Nitpick comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java`:
- Around line 722-739: The new anonymous Callable in VmCascadeExtension
introduces a raw TypedQuery JPQL branch to fetch VMs by L2 network UUIDs;
extract this into a shared helper (e.g., queryVmsByNetworkRange or
findVmsByL2NetworkUuids) and reimplement it using the project-standard
Q.New(...) / SQL.New(...) API used by the existing L3 branch, reusing the same
filters (vm.type == VmInstanceConstant.USER_VM_TYPE, vm.state in {Stopped,
Paused, Running, Destroyed}, join through VmNicVO -> L3NetworkVO -> L2Network
UUIDs) instead of adding another TypedQuery; update the Callable to call that
helper and remove the JPQL string so the code follows the coding guideline of
avoiding TypedQuery.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)
Review profile: CHILL
Plan: Pro
Run ID: 4f4af80d-cd65-4c77-b7ad-ec1b4e60ef76
📒 Files selected for processing (1)
compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java
| List<String> l3Uuids = Q.New(L3NetworkVO.class).in(L3NetworkVO_.l2NetworkUuid, l2Uuids) | ||
| .select(L3NetworkVO_.uuid).listValues(); | ||
| if (l3Uuids.isEmpty()) { | ||
| return msgs; | ||
| } |
There was a problem hiding this comment.
L2 删除路径不要依赖仍然存在的 L3NetworkVO 记录。
这里一处先通过 L2 -> L3 现查 UUID,另一处再通过 nic.l3NetworkUuid = l3.uuid 反查 VM。结合 header/src/main/java/org/zstack/header/vm/VmNicVO.java:35-39,L3 删除时 VmNicVO.l3NetworkUuid 会被 SET_NULL;如果同一条 cascade 里 L3 已先处理,这两个查询都会漏掉目标 NIC/VM,最终这次新增的 detach 逻辑就不会触发。建议把 L2 删除命中的 NIC/VM 一次性算出来并沿用,避免在后续步骤再次依赖 L3 行仍在库里。
Also applies to: 726-731
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java` around
lines 339 - 343, The current L2 delete flow first derives l3Uuids via
Q.New(L3NetworkVO).in(L3NetworkVO_.l2NetworkUuid, l2Uuids) and later re-queries
NIC/VMs using nic.l3NetworkUuid, but VmNicVO.l3NetworkUuid is SET_NULL on L3
delete so NICs can be missed if L3s were processed earlier; instead, compute the
affected VmNic/VM set once up-front for the L2 cascade (e.g. query VmNicVO by
the L2 identifier(s) or by joined criteria that do not rely on L3NetworkVO
existence), store that NIC/VM list (used where msgs is built) and reuse it in
subsequent steps instead of re-querying via L3NetworkVO/l3.uuid so the detach
logic always runs for those NICs/VMs.
Resolves: ZCF-2063 Change-Id: Ib7ed3b9a111bad1ec0d6f76ece259bfd25444bb7
<fix>[zns]: add ZNS error code ORG_ZSTACK_NETWORK_ZNS_10011 See merge request zstackio/zstack!9584
AttachL2NetworkToCluster incorrectly blocks ZNS L2
networks. Three validation points assume all L2s use
host physical interfaces. ZNS is an SDN controller,
physicalInterface is always "" and meaningless.
Fix 1: L2NetworkApiInterceptor - skip vSwitchType
conflict check for ZNS L2
Fix 2: KVMApiInterceptor - skip vlan device name
length check for ZNS L2
Fix 3: L2NoVlanNetwork - skip physicalInterface
conflict for ZNS L2NoVlan, exclude ZNS
L2Vlan from non-SDN overlap query
Resolves: ZCF-2073
Change-Id: I757966777a64736a6d646178786e6b70617a7a77
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7998ea6 to
4baf00d
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (4)
compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java (2)
44-45: 未使用的字段vmMgr
VmInstanceManager vmMgr已注入但在本文件中从未使用。请移除该字段以保持代码整洁。♻️ 建议移除未使用的字段
-@Autowired -private VmInstanceManager vmMgr; `@Autowired` private PluginRegistry pluginRgty;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java` around lines 44 - 45, Remove the unused injected field VmInstanceManager vmMgr from the VmDetachNicFlow class; locate the private field declaration "private VmInstanceManager vmMgr" annotated with `@Autowired` in VmDetachNicFlow and delete that field and its `@Autowired` annotation to clean up unused code and imports.
120-125: 与VmReturnReleaseNicFlow中的实现存在细微差异此处仅检查
exts.isEmpty(),而VmReturnReleaseNicFlow.callReleaseSdnNics还额外检查了nics.isEmpty()。虽然当前调用方 (line 101) 总是传入 singletonList,不会为空,但为保持一致性和防御性编程,建议统一检查条件。♻️ 建议与 VmReturnReleaseNicFlow 保持一致
private void callReleaseSdnNics(List<VmNicInventory> nics, Completion completion) { List<AfterAllocateSdnNicExtensionPoint> exts = pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class); - if (exts.isEmpty()) { + if (exts.isEmpty() || nics.isEmpty()) { completion.success(); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java` around lines 120 - 125, The callReleaseSdnNics method in VmDetachNicFlow only checks whether extension list is empty but omits the defensive check for an empty nics list (VmReturnReleaseNicFlow.callReleaseSdnNics also checks nics.isEmpty()); update VmDetachNicFlow.callReleaseSdnNics to return completion.success() when either pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class) is empty OR the passed-in nics list is empty, mirroring the logic in VmReturnReleaseNicFlow and preventing unnecessary extension invocation when nics is empty.compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java (2)
40-41: 未使用的字段vmMgr
VmInstanceManager vmMgr已注入但在本文件中从未使用。请移除该字段以保持代码整洁。♻️ 建议移除未使用的字段
`@Autowired` protected PluginRegistry pluginRgty; -@Autowired -protected VmInstanceManager vmMgr;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java` around lines 40 - 41, Remove the unused injected field by deleting the protected VmInstanceManager vmMgr declaration (including its `@Autowired` annotation) from the VmReturnReleaseNicFlow class; also remove any now-unused import for VmInstanceManager and ensure there are no remaining references to vmMgr elsewhere in this class.
120-146:callReleaseSdnNics方法与VmDetachNicFlow中高度重复该方法与
VmDetachNicFlow.java中的callReleaseSdnNics实现几乎完全相同。建议将此方法提取到公共工具类或基类中,以遵循 DRY 原则并减少维护成本。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java` around lines 120 - 146, The callReleaseSdnNics implementation is duplicated between VmReturnReleaseNicFlow and VmDetachNicFlow; extract the shared logic into a single reusable utility or base method (e.g., a static helper in a new SdnNicReleaseUtils or a protected method on a common base class) that accepts List<VmNicInventory> nics, PluginRegistry/PluginRgty access, and a Completion, and have both VmReturnReleaseNicFlow.callReleaseSdnNics and VmDetachNicFlow.delegate to that new method; ensure the helper uses the same AfterAllocateSdnNicExtensionPoint iteration, Completion/While/WhileDoneCompletion behavior, and logging semantics so callers simply forward nics and completion to the consolidated routine.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java`:
- Around line 44-45: Remove the unused injected field VmInstanceManager vmMgr
from the VmDetachNicFlow class; locate the private field declaration "private
VmInstanceManager vmMgr" annotated with `@Autowired` in VmDetachNicFlow and delete
that field and its `@Autowired` annotation to clean up unused code and imports.
- Around line 120-125: The callReleaseSdnNics method in VmDetachNicFlow only
checks whether extension list is empty but omits the defensive check for an
empty nics list (VmReturnReleaseNicFlow.callReleaseSdnNics also checks
nics.isEmpty()); update VmDetachNicFlow.callReleaseSdnNics to return
completion.success() when either
pluginRgty.getExtensionList(AfterAllocateSdnNicExtensionPoint.class) is empty OR
the passed-in nics list is empty, mirroring the logic in VmReturnReleaseNicFlow
and preventing unnecessary extension invocation when nics is empty.
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java`:
- Around line 40-41: Remove the unused injected field by deleting the protected
VmInstanceManager vmMgr declaration (including its `@Autowired` annotation) from
the VmReturnReleaseNicFlow class; also remove any now-unused import for
VmInstanceManager and ensure there are no remaining references to vmMgr
elsewhere in this class.
- Around line 120-146: The callReleaseSdnNics implementation is duplicated
between VmReturnReleaseNicFlow and VmDetachNicFlow; extract the shared logic
into a single reusable utility or base method (e.g., a static helper in a new
SdnNicReleaseUtils or a protected method on a common base class) that accepts
List<VmNicInventory> nics, PluginRegistry/PluginRgty access, and a Completion,
and have both VmReturnReleaseNicFlow.callReleaseSdnNics and
VmDetachNicFlow.delegate to that new method; ensure the helper uses the same
AfterAllocateSdnNicExtensionPoint iteration,
Completion/While/WhileDoneCompletion behavior, and logging semantics so callers
simply forward nics and completion to the consolidated routine.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)
Review profile: CHILL
Plan: Pro
Run ID: d0ce3bb6-1724-4686-b6d8-7b6016739e7e
📒 Files selected for processing (3)
compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.javacompute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java
✅ Files skipped from review due to trivial changes (1)
- compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java
<fix>[l2]: skip ZNS L2 in attach interceptors See merge request zstackio/zstack!9589
Handle blank or omitted type in APIChangeL2NetworkVlanIdMsg by normalizing to the current L2 type before validation checks. Resolves: ZSTAC-2074 Change-Id: Icf960d0766b726047d8613305042cfa14e120c61
<fix>[network]: normalize ZNS L2 change target type handling See merge request zstackio/zstack!9613
4baf00d to
c34e420
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java (1)
157-252:⚠️ Potential issue | 🟠 MajorGeneve 类型缺少后续验证逻辑。
当前代码在第160行允许
L2_GENEVE_NETWORK_TYPE通过初始检查,但后续只有targetIsVlan和targetIsNoVlan的验证分支(分别在167-212行和213-251行),targetIsGeneve分支完全缺失。这意味着 Geneve 类型的网络变更请求会跳过所有冲突检测和参数验证,直接通过拦截器。虽然定义了相关错误码(如
ORG_ZSTACK_NETWORK_ZNS_10014"Geneve type can only change VNI, not L2 type" 等),但这些错误码在代码中并未被使用。需要补充 Geneve 类型的完整验证逻辑,或明确说明 Geneve 的验证由其他拦截器(如 ZNS 模块的拦截器)单独处理。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java` around lines 157 - 252, The interceptor currently allows targetIsGeneve to pass initial type check but lacks a corresponding validation branch; add a branch for targetIsGeneve in the ChangeL2NetworkVlanId handling (near the existing targetIsVlan / targetIsNoVlan branches) that enforces Geneve-specific rules: require/validate VNI (use msg.getVni or the proper VNI accessor), forbid changing L2 type where applicable, run the same cluster/physical-interface conflict checks as other branches if relevant, and throw the appropriate ApiMessageInterceptionException using the existing error codes (e.g. ORG_ZSTACK_NETWORK_ZNS_10014 or other defined codes) when validation fails; alternatively, if Geneve validation is delegated to ZNS module, add a clear comment and an explicit no-op branch for targetIsGeneve referencing that delegation so the behavior is explicit.
🧹 Nitpick comments (6)
compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java (2)
40-43: 未使用的注入字段vmMgr
vmMgr字段已通过@Autowired注入,但在整个文件中从未被引用。这是无用代码,应当移除。♻️ 建议移除未使用的字段
`@Autowired` protected VmInstanceDeletionPolicyManager deletionPolicyMgr; -@Autowired -protected VmInstanceManager vmMgr; `@Autowired` protected PluginRegistry pluginRgty;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java` around lines 40 - 43, The injected field VmInstanceManager vmMgr in class VmReturnReleaseNicFlow is never used; remove the unused `@Autowired` field declaration (VmInstanceManager vmMgr) from VmReturnReleaseNicFlow and also remove any now-unused import of VmInstanceManager if present to keep the code clean.
120-146:callReleaseSdnNics方法与VmDetachNicFlow存在重复代码该方法的实现与
VmDetachNicFlow.java中的callReleaseSdnNics()几乎完全相同。建议将此公共逻辑抽取到VmInstanceManager或创建一个共享的工具类,以符合 DRY 原则并便于后续维护。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java` around lines 120 - 146, VmReturnReleaseNicFlow.callReleaseSdnNics duplicates logic found in VmDetachNicFlow.callReleaseSdnNics; extract the shared iteration over AfterAllocateSdnNicExtensionPoint and its Completion handling into a single reusable method (either a new static helper in a shared utility class or an instance method on VmInstanceManager) and replace both VmReturnReleaseNicFlow.callReleaseSdnNics and VmDetachNicFlow.callReleaseSdnNics to delegate to that new method; ensure the new method signature accepts List<VmNicInventory> and a Completion and preserves the existing behavior (early return when extensions or nics are empty, warn-on-fail, call completion.success() on done).compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java (3)
101-101: 建议使用 import 而非内联全限定类名
java.util.Collections.singletonList(nic)使用了全限定类名。建议在文件头部添加import java.util.Collections;,然后直接使用Collections.singletonList(nic),以保持代码风格一致性。♻️ 建议的修改
在文件头部添加 import:
import java.util.Collections;然后修改调用处:
-callReleaseSdnNics(java.util.Collections.singletonList(nic), new Completion(trigger) { +callReleaseSdnNics(Collections.singletonList(nic), new Completion(trigger) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java` at line 101, 在 callReleaseSdnNics 调用处使用了全限定名 java.util.Collections.singletonList(nic);请在文件顶部添加 import java.util.Collections; 并将该调用替换为 Collections.singletonList(nic) 以保持项目的导入风格一致并提高可读性;定位符参考:方法调用 callReleaseSdnNics(...) 和参数 singletonList 的替换位置。
44-47: 未使用的注入字段vmMgr与
VmReturnReleaseNicFlow.java相同,vmMgr字段已注入但从未在此文件中使用。建议移除。♻️ 建议移除未使用的字段
`@Autowired` private VmInstanceDeviceManager vidm; -@Autowired -private VmInstanceManager vmMgr; `@Autowired` private PluginRegistry pluginRgty;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java` around lines 44 - 47, 类 VmDetachNicFlow 中注入的字段 vmMgr 未被使用,请删除该字段声明(private VmInstanceManager vmMgr;)及其相关注入注解,确保同时移除任何不再需要的导入或引用,以保持类只保留实际使用的注入(例如 PluginRegistry pluginRgty);保存并运行编译/静态检查以确认没有残留引用。
120-146: 重复的callReleaseSdnNics实现此方法与
VmReturnReleaseNicFlow.java中的实现高度重复。如前所述,建议将此逻辑抽取到共享位置(如VmInstanceManager或工具类),避免重复维护。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java` around lines 120 - 146, The callReleaseSdnNics implementation in VmDetachNicFlow is duplicated from VmReturnReleaseNicFlow; extract the shared logic that queries pluginRgty for AfterAllocateSdnNicExtensionPoint and runs the While<> loop into a single reusable method (e.g., VmInstanceManager.releaseSdnNics or a SdnNicHelper.releaseSdnNics) that accepts List<VmNicInventory> and Completion, move the While<>/ext.releaseSdnNics handling there, and replace the duplicate implementations in VmDetachNicFlow and VmReturnReleaseNicFlow with calls to the new shared method (keeping the same error handling behavior and use of pluginRgty and AfterAllocateSdnNicExtensionPoint).network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java (1)
4-4: 未使用的导入。
org.apache.poi.util.StringUtil未在代码中使用,只使用了org.apache.commons.lang.StringUtils。建议移除此导入。♻️ 移除未使用的导入
-import org.apache.poi.util.StringUtil;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java` at line 4, Remove the unused import org.apache.poi.util.StringUtil from L2NetworkApiInterceptor (the class referenced is L2NetworkApiInterceptor.java) — the code already uses org.apache.commons.lang.StringUtils, so delete the unused org.apache.poi.util.StringUtil import to clean up imports and avoid warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java`:
- Line 84: The call to dbf.findByUuid(nic.getUuid(), VmNicVO.class) in
VmReturnReleaseNicFlow can return null when a NIC was already deleted; avoid
adding nulls to nicsToDelete and prevent NPEs by checking the result (VmNicVO)
before adding it to nicsToDelete, e.g., only add when non-null, or filter the
list prior to calling nicsToDelete.forEach(dbf::remove); ensure references to
dbf.remove and nicsToDelete are protected by this null check.
---
Outside diff comments:
In `@network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java`:
- Around line 157-252: The interceptor currently allows targetIsGeneve to pass
initial type check but lacks a corresponding validation branch; add a branch for
targetIsGeneve in the ChangeL2NetworkVlanId handling (near the existing
targetIsVlan / targetIsNoVlan branches) that enforces Geneve-specific rules:
require/validate VNI (use msg.getVni or the proper VNI accessor), forbid
changing L2 type where applicable, run the same cluster/physical-interface
conflict checks as other branches if relevant, and throw the appropriate
ApiMessageInterceptionException using the existing error codes (e.g.
ORG_ZSTACK_NETWORK_ZNS_10014 or other defined codes) when validation fails;
alternatively, if Geneve validation is delegated to ZNS module, add a clear
comment and an explicit no-op branch for targetIsGeneve referencing that
delegation so the behavior is explicit.
---
Nitpick comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java`:
- Line 101: 在 callReleaseSdnNics 调用处使用了全限定名
java.util.Collections.singletonList(nic);请在文件顶部添加 import java.util.Collections;
并将该调用替换为 Collections.singletonList(nic) 以保持项目的导入风格一致并提高可读性;定位符参考:方法调用
callReleaseSdnNics(...) 和参数 singletonList 的替换位置。
- Around line 44-47: 类 VmDetachNicFlow 中注入的字段 vmMgr 未被使用,请删除该字段声明(private
VmInstanceManager vmMgr;)及其相关注入注解,确保同时移除任何不再需要的导入或引用,以保持类只保留实际使用的注入(例如
PluginRegistry pluginRgty);保存并运行编译/静态检查以确认没有残留引用。
- Around line 120-146: The callReleaseSdnNics implementation in VmDetachNicFlow
is duplicated from VmReturnReleaseNicFlow; extract the shared logic that queries
pluginRgty for AfterAllocateSdnNicExtensionPoint and runs the While<> loop into
a single reusable method (e.g., VmInstanceManager.releaseSdnNics or a
SdnNicHelper.releaseSdnNics) that accepts List<VmNicInventory> and Completion,
move the While<>/ext.releaseSdnNics handling there, and replace the duplicate
implementations in VmDetachNicFlow and VmReturnReleaseNicFlow with calls to the
new shared method (keeping the same error handling behavior and use of
pluginRgty and AfterAllocateSdnNicExtensionPoint).
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java`:
- Around line 40-43: The injected field VmInstanceManager vmMgr in class
VmReturnReleaseNicFlow is never used; remove the unused `@Autowired` field
declaration (VmInstanceManager vmMgr) from VmReturnReleaseNicFlow and also
remove any now-unused import of VmInstanceManager if present to keep the code
clean.
- Around line 120-146: VmReturnReleaseNicFlow.callReleaseSdnNics duplicates
logic found in VmDetachNicFlow.callReleaseSdnNics; extract the shared iteration
over AfterAllocateSdnNicExtensionPoint and its Completion handling into a single
reusable method (either a new static helper in a shared utility class or an
instance method on VmInstanceManager) and replace both
VmReturnReleaseNicFlow.callReleaseSdnNics and VmDetachNicFlow.callReleaseSdnNics
to delegate to that new method; ensure the new method signature accepts
List<VmNicInventory> and a Completion and preserves the existing behavior (early
return when extensions or nics are empty, warn-on-fail, call
completion.success() on done).
In `@network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java`:
- Line 4: Remove the unused import org.apache.poi.util.StringUtil from
L2NetworkApiInterceptor (the class referenced is L2NetworkApiInterceptor.java) —
the code already uses org.apache.commons.lang.StringUtils, so delete the unused
org.apache.poi.util.StringUtil import to clean up imports and avoid warnings.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)
Review profile: CHILL
Plan: Pro
Run ID: 0b838d47-3fd8-4254-9f0c-5680937b3303
📒 Files selected for processing (10)
compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.javacompute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.javanetwork/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.javanetwork/src/main/java/org/zstack/network/l2/L2NetworkExtensionPointEmitter.javanetwork/src/main/java/org/zstack/network/l2/L2NetworkHostHelper.javanetwork/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.javaplugin/kvm/src/main/java/org/zstack/kvm/KVMApiInterceptor.javaplugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkCheckerImpl.javautils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
✅ Files skipped from review due to trivial changes (2)
- plugin/kvm/src/main/java/org/zstack/kvm/KVMApiInterceptor.java
- compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java
🚧 Files skipped from review as they are similar to previous changes (1)
- utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
| List<VmNicInventory> releasedNics = new ArrayList<>(); | ||
| List<VmNicVO> nicsToDelete = new ArrayList<>(); | ||
| for (VmNicInventory nic : spec.getVmInventory().getVmNics()) { | ||
| VmNicVO vo = dbf.findByUuid(nic.getUuid(), VmNicVO.class); |
There was a problem hiding this comment.
dbf.findByUuid 可能返回 null
当 NIC 已被删除或不存在时,dbf.findByUuid(nic.getUuid(), VmNicVO.class) 会返回 null。此 null 值会被添加到 nicsToDelete 列表中,随后在 nicsToDelete.forEach(dbf::remove) 时可能导致 NPE 或不可预期的行为。
🛡️ 建议添加空值检查
for (VmNicInventory nic : spec.getVmInventory().getVmNics()) {
VmNicVO vo = dbf.findByUuid(nic.getUuid(), VmNicVO.class);
+ if (vo == null) {
+ releasedNics.add(nic);
+ continue;
+ }
if (VmInstanceConstant.USER_VM_TYPE.equals(spec.getVmInventory().getType())) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java` at
line 84, The call to dbf.findByUuid(nic.getUuid(), VmNicVO.class) in
VmReturnReleaseNicFlow can return null when a NIC was already deleted; avoid
adding nulls to nicsToDelete and prevent NPEs by checking the result (VmNicVO)
before adding it to nicsToDelete, e.g., only add when non-null, or filter the
list prior to calling nicsToDelete.forEach(dbf::remove); ensure references to
dbf.remove and nicsToDelete are protected by this null check.
Release SDN NICs before removing VmNicVO. Keep NIC deletion on release failure. Resolves: ZCF-2047 Change-Id: I83f534ea19849467a728e3b6fb9ee2f6bb43bb7e Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
c34e420 to
66c8e13
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java (1)
84-113:⚠️ Potential issue | 🟠 Major先过滤掉已不存在的
VmNicVO。Line 84 的
dbf.findByUuid(...)可能返回null。当前代码在非Direct分支会直接解引用vo,其余分支则会把null放进nicsToDelete,随后在 Line 105 / Line 112 调dbf.remove。L2 NIC 若已先被卸载,这里会把回收流程打成 NPE。建议修改
for (VmNicInventory nic : spec.getVmInventory().getVmNics()) { VmNicVO vo = dbf.findByUuid(nic.getUuid(), VmNicVO.class); + if (vo == null) { + releasedNics.add(nic); + continue; + } + if (VmInstanceConstant.USER_VM_TYPE.equals(spec.getVmInventory().getType())) { VmInstanceDeletionPolicy deletionPolicy = getDeletionPolicy(spec, data); if (deletionPolicy == VmInstanceDeletionPolicy.Direct) { nicsToDelete.add(vo); } else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java` around lines 84 - 113, The code in VmReturnReleaseNicFlow calls dbf.findByUuid(...) which can return null; avoid NPE by filtering out missing VmNicVOs before using or adding them to nicsToDelete/releasedNics: when dbf.findByUuid(nic.getUuid(), VmNicVO.class) returns null, skip that nic (optionally log a warning) and do not add null to nicsToDelete or releasedNics, and ensure the later callSite that removes entries (nicsToDelete.forEach(dbf::remove)) only iterates real VmNicVO instances; update the loop that builds nicsToDelete/releasedNics around callReleaseSdnNics accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java`:
- Around line 127-143: In VmDetachNicFlow, the releaseSdnNics call inside the
new While<>(exts).each(...) swallows failures (logger.warn and always
wcomp.done()) and the enclosing WhileDoneCompletion always calls
completion.success(), allowing NIC DB deletion to proceed; change the failure
handling in the Completion passed to ext.releaseSdnNics so that on
fail(ErrorCode) you propagate the error to the outer completion (e.g.,
collect/forward the ErrorCode into the WhileDoneCompletion and call
completion.fail(...)) instead of only logging, or alternatively persist a
compensation/retry record before allowing completion.success() to proceed;
update the Completion.fail path and the WhileDoneCompletion.done(...) logic to
ensure failures from releaseSdnNics prevent unconditional completion.success()
and trigger retry/compensation handling.
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java`:
- Around line 127-143: In VmReturnReleaseNicFlow, the ext.releaseSdnNics(...)
fail handler currently only logs and calls wcomp.done(), and the outer
WhileDoneCompletion always calls completion.success(), which lets callers delete
local VmNicVO even when SDN release failed; change the flow to propagate errors:
collect ErrorCodes from ext.releaseSdnNics within the While (or record failures
in a shared ErrorCodeList), and in WhileDoneCompletion.done inspect that list
and call completion.fail(...) with a composed ErrorCode when any SDN release
failed (or alternatively enqueue a compensating retry task before returning
success); ensure changes touch the ext.releaseSdnNics fail path, the
WhileDoneCompletion.done logic that currently calls completion.success(), and
the completion passed into VmReturnReleaseNicFlow so the caller receives
failures instead of silent success.
---
Duplicate comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java`:
- Around line 84-113: The code in VmReturnReleaseNicFlow calls
dbf.findByUuid(...) which can return null; avoid NPE by filtering out missing
VmNicVOs before using or adding them to nicsToDelete/releasedNics: when
dbf.findByUuid(nic.getUuid(), VmNicVO.class) returns null, skip that nic
(optionally log a warning) and do not add null to nicsToDelete or releasedNics,
and ensure the later callSite that removes entries
(nicsToDelete.forEach(dbf::remove)) only iterates real VmNicVO instances; update
the loop that builds nicsToDelete/releasedNics around callReleaseSdnNics
accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: http://open.zstack.ai:20001/code-reviews/zstack-cloud.yaml (via .coderabbit.yaml)
Review profile: CHILL
Plan: Pro
Run ID: 78687c16-4e2f-4068-a339-f5b02be6d92a
📒 Files selected for processing (6)
compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.javacompute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.javacompute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.javanetwork/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.javanetwork/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.javautils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
🚧 Files skipped from review as they are similar to previous changes (4)
- network/src/main/java/org/zstack/network/l2/L2NoVlanNetwork.java
- network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java
- compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java
- utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
| new While<>(exts).each((ext, wcomp) -> { | ||
| ext.releaseSdnNics(nics, new Completion(wcomp) { | ||
| @Override | ||
| public void success() { | ||
| wcomp.done(); | ||
| } | ||
|
|
||
| @Override | ||
| public void fail(ErrorCode errorCode) { | ||
| logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode)); | ||
| wcomp.done(); | ||
| } | ||
| }); | ||
| }).run(new WhileDoneCompletion(completion) { | ||
| @Override | ||
| public void done(ErrorCodeList errorCodeList) { | ||
| completion.success(); |
There was a problem hiding this comment.
这里同样把 SDN 释放失败吞掉了。
ext.releaseSdnNics(...) 失败后只会 warn 并继续,Line 143 再统一走 completion.success();于是调用方仍会在 Line 104 / Line 112 删除 NIC 记录并 trigger.next()。如果外部 SDN 控制器释放失败,这里会直接丢失本地重试入口,容易留下孤儿 SDN 资源。建议改成失败透传,或者在删除数据库记录前先持久化补偿/重试信息。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@compute/src/main/java/org/zstack/compute/vm/VmDetachNicFlow.java` around
lines 127 - 143, In VmDetachNicFlow, the releaseSdnNics call inside the new
While<>(exts).each(...) swallows failures (logger.warn and always wcomp.done())
and the enclosing WhileDoneCompletion always calls completion.success(),
allowing NIC DB deletion to proceed; change the failure handling in the
Completion passed to ext.releaseSdnNics so that on fail(ErrorCode) you propagate
the error to the outer completion (e.g., collect/forward the ErrorCode into the
WhileDoneCompletion and call completion.fail(...)) instead of only logging, or
alternatively persist a compensation/retry record before allowing
completion.success() to proceed; update the Completion.fail path and the
WhileDoneCompletion.done(...) logic to ensure failures from releaseSdnNics
prevent unconditional completion.success() and trigger retry/compensation
handling.
| new While<>(exts).each((ext, wcomp) -> { | ||
| ext.releaseSdnNics(nics, new Completion(wcomp) { | ||
| @Override | ||
| public void success() { | ||
| wcomp.done(); | ||
| } | ||
|
|
||
| @Override | ||
| public void fail(ErrorCode errorCode) { | ||
| logger.warn(String.format("releaseSdnNics extension failed: %s, continue", errorCode)); | ||
| wcomp.done(); | ||
| } | ||
| }); | ||
| }).run(new WhileDoneCompletion(completion) { | ||
| @Override | ||
| public void done(ErrorCodeList errorCodeList) { | ||
| completion.success(); |
There was a problem hiding this comment.
不要在 SDN 释放失败后仍然无条件删除本地 NIC。
这里对 ext.releaseSdnNics(...) 的 fail() 只记日志并 wcomp.done(),Line 143 又无条件 completion.success()。结果是外部 SDN 清理失败时,调用方仍会继续删除/清空本地 VmNicVO,云平台和 SDN 控制器状态会分叉,而且没有重试抓手。建议至少把失败透传给调用方,或在继续删本地状态前先落一条可重试的补偿任务。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@compute/src/main/java/org/zstack/compute/vm/VmReturnReleaseNicFlow.java`
around lines 127 - 143, In VmReturnReleaseNicFlow, the ext.releaseSdnNics(...)
fail handler currently only logs and calls wcomp.done(), and the outer
WhileDoneCompletion always calls completion.success(), which lets callers delete
local VmNicVO even when SDN release failed; change the flow to propagate errors:
collect ErrorCodes from ext.releaseSdnNics within the While (or record failures
in a shared ErrorCodeList), and in WhileDoneCompletion.done inspect that list
and call completion.fail(...) with a composed ErrorCode when any SDN release
failed (or alternatively enqueue a compensating retry task before returning
success); ensure changes touch the ext.releaseSdnNics fail path, the
WhileDoneCompletion.done logic that currently calls completion.success(), and
the completion passed into VmReturnReleaseNicFlow so the caller receives
failures instead of silent success.
Resolves: ZCF-1365
Change-Id: I73687962636e7871626e687761626d6661716668
sync from gitlab !9574